<?php

Raise::load('core.RaiseHelper');
Raise::load('core.RaiseDateTime');
Raise::load('core.RaiseStream');
Raise::load('core.RaiseHttpHeaderCollection');
Raise::load('core.RaiseCollection');
Raise::load('core.RaiseApplication');
Raise::load('core.RaiseText');
Raise::load('core.RaiseVariable');
Raise::load('core.RaiseFileStream');
Raise::load('core.RaiseServer');
Raise::load('core.Url');

/**
 * RaiseHttpRequest class
 * defines a HTTP Request
 *
 * @author Sam-Mauris Yong <hellclanner at live dot com>
 * @license http://www.opensource.org/licenses/bsd-license New BSD License
 * @package Raise.Core.HTTP
 * @since 1.2
 */
class RaiseHttpRequest extends RaiseHelper {

    const METHOD_POST = 'POST';
    const METHOD_HEAD = 'HEAD';
    const METHOD_GET = 'GET';
    const METHOD_PUT = 'PUT';
    const METHOD_DELETE = 'DELETE';
    const METHOD_TRACE = 'TRACE';
    const METHOD_CONNECT = 'CONNECT';
    const METHOD_PATCH = 'PATCH';

    const HTTP_1_1 = 'HTTP/1.1';
    const HTTP_1_0 = 'HTTP/1.0';

    /**
     * The method of request, e.g. GET, POST, HEAD
     * @var string
     */
    private $method;

    /**
     * The requested path / URI e.g. /example/path/to/file.php
     * @var string
     */
    private $uri;

    /**
     * The HTTP version called upon e.g. HTTP/1.0 or HTTP/1.1
     * @var string
     */
    private $version;

    /**
     * Body of the Request
     * @var RaiseStream
     */
    private $body;

    /**
     * DateTime the request was made
     * @var RaiseDateTime
     */
    private $time;

    /**
     * An array of the HTTP headers in the HTTP Response
     * @var RaiseHttpHeaderCollection
     */
    private $headers;

    /**
     * Whether the request is via HTTPS or not
     * @var boolean
     */
    private $https;

    /**
     * Cookies pertaining to this request
     * @var RaiseCollection
     */
    private $cookies;

    /**
     * An array of POST data related to this request
     * @var RaiseCollection
     */
    private $post;

    /**
     * An array of GET data related to this request
     * @var RaiseCollection
     */
    private $get;

    /**
     * The RaiseApplication that owns this Request
     * @var RaiseApplication
     */
    private $application;

    /**
     * Represents the current RaiseHttpRequest - for caching use.
     * @var RaiseHttpRequest
     */
    private static $current = false;

    /**
     * Create a new RaiseHttpRequest
     * @param string $s (optional) The HTTP request string to parse from.
     */
    public function __construct($s = null){
        $this->cookies = new RaiseCollection();
        $this->post = new RaiseCollection();
        $this->get = new RaiseCollection();
        $this->headers = new RaiseHttpHeaderCollection();
        if($s){
            $this->parse($s);
        }
    }

    /**
     * Parse the HTTP request in the form of a string into this object
     * @param string $s
     */
    private function parse($s){
        $s = RaiseText::neutralizeNewlines($s);
        $lines = explode(RaiseText::NEWLINE_NIX, $s);
        if(count($lines) > 1){
            $requestLine = $lines[0];
            $itp = explode(' ', $requestLine);
            $this->method(trim($itp[0]));
            $this->uri(trim($itp[1]));
            $this->version(trim($itp[2]));
            $last = count($lines)-1;
            $this->body($lines[$last]);
            unset($lines[0]);
            unset($lines[$last]);
            unset($last);

            foreach($lines as $l){
                $c = strpos($l, ':');
                if($c){
                    $key = trim(substr($l, 0, $c));
                    $value = trim(substr($l, $c + 1));
                    $this->headers()->add($key, $value, false);
                }
            }
        }
    }

    /**
     * Get the entire Request line
     * @return string
     */
    public function requestLine() {
        return $this->method() . ' ' . $this->uri() . ' ' . $this->version();
    }

    /**
     * Get or set the URI of the HTTP request
     * @param string $u (optional) If set, the new value will be set.
     * @return string
     */
    public function uri($u = null){
        if(func_num_args() == 1){
            $this->uri = $u;
        }
        return $this->uri;
    }

    /**
     * Get or set the method of the HTTP request
     * @param string $m (optional) If set, the new value will be set.
     * @return string
     */
    public function method($m = null){
        if(func_num_args() == 1){
            $this->method = $m;
        }
        return $this->method;
    }

    /**
     * Get or set the version of the HTTP request
     * @param string $v (optional) If set, the new value will be set.
     * @return string
     */
    public function version($v = null){
        if(func_num_args() == 1){
            $this->version = $v;
        }
        return $this->version;
    }

    /**
     * Get or set the RaiseStream that represents body of the HTTP request
     * @param RaiseStream $b (optional) If set, the new value will be set.
     * @return RaiseStream
     */
    public function body($b = null){
        if(func_num_args() == 1){
            $this->body = $b;
        }
        return $this->body;
    }

    /**
     * Get or set the date/time of the HTTP request
     * @param RaiseDateTime $t (optional) If set, the new value will be set.
     * @return RaiseDateTime
     */
    public function time($t = null){
        if(func_num_args() == 1){
            $this->time = $t;
        }
        return $this->time;
    }

    /**
     * Get or set the RaiseApplication that owns this Request
     * @param RaiseApplication $app (optional) If set, the new value will be set.
     * @return RaiseApplication
     */
    public function application($app = null){
        if(func_num_args() == 1){
            if(!($app instanceof RaiseApplication)){
                throw new RaiseInvalidArgumentException('RaiseHttpRequest::application() expecting $app to be an instance of RaiseApplication, ' . RaiseVariable::typeInfo($app) . ' given instead.');
            }
            $this->application = $app;
        }
        return $this->application;
    }

    public function __toString(){
        $buffer = '';
        $buffer .= $this->requestLine()."\r\n";
        foreach ($this->headers() as $k => $h) {
            if (is_array($h)) {
                foreach ($h as $d) {
                    $buffer .= $k . ': ' . $d . "\n";
                }
            } else {
                    $buffer .= $k . ': ' . $h . "\n";
            }
        }
        $buffer .= "\n";
        $buffer .= $this->body();
        return $buffer;
    }

    /**
     * Get or set whether this request is HTTPS or not
     * @param boolean $h (optional) If set, the new value will be set.
     * @return boolean
     */
    public function https($h = null){
        if(func_num_args() == 1){
            $this->https = $h;
        }
        return $this->https;
    }

    /**
     * Get or set the RaiseHttpHeaderCollection that contains all the headers of the HTTP request
     * @param RaiseHttpHeaderCollection $h (optional) If set, the new value will be set.
     * @return RaiseHttpHeaderCollection
     */
    public function headers($h = null){
        if(func_num_args() == 1){
            if(!($app instanceof RaiseHttpHeaderCollection)){
                throw new RaiseInvalidArgumentException('Invalid parameter passed when setting headers property. Expecting RaiseHttpHeaderCollection.');
            }
            $this->headers = $h;
        }
        return $this->headers;
    }

    /**
     * Get a particular POST value using its corresponding key or the entire collection of POST data
     * @param string $key (optional) Name of the POST value to receive. If not set, the function will return the entire collection of the POST data.
     * @param boolean $raw (optional) Set if the method should return the variable raw without any processing. Defaults to FALSE.
     * @return mixed|RaiseCollection
     */
    public function post($key = null, $raw = false) {
        if($key == null){
            return $this->post;
        }else{
            if(!$this->post->keyExists($key)){
                return null;
            }
            $value = $this->post->get($key);
            if ($raw || (!$raw && !get_magic_quotes_gpc())) {
                return $value;
            } else {
                if ($value instanceof RaiseCollection || RaiseVariable::isType($value, RaiseVariable::TYPEARRAY)) {
                    array_walk_recursive($value, create_function('&$v, $key',
                                    '$v = stripslashes($v);'));
                    return $value;
                } else {
                    return stripslashes($value);
                }
            }
        }
    }

    /**
     * Get a particular Cookie value using its corresponding key or the entire collection of Cookies that was received from the browser
     * @return RaiseTypedCollection
     */
    public function cookies() {
        return $this->cookies;
    }

    /**
     * Get a particular GET value using its corresponding key or the entire collection of GET data
     * @param string $key (optional) Name of the GET value to receive. If not set, the function will return the entire collection of the POST data.
     * @param boolean $raw (optional) Set if the method should return the variable raw without any processing. Defaults to FALSE.
     * @return mixed|RaiseCollection
     */
    public function get($key = null, $raw = false) {
        if($key == null){
            return $this->get;
        }else{
            if(!$this->get->keyExists($key)){
                return null;
            }
            $value = $this->get->get($key);
            if ($raw || (!$raw && !get_magic_quotes_gpc())) {
                return $value;
            } else {
                if ($value instanceof RaiseCollection || RaiseVariable::isType($value, RaiseVariable::TYPEARRAY)) {
                    array_walk_recursive($value, create_function('&$v, $key',
                                    '$v = stripslashes($v);'));
                    return $value;
                } else {
                    return stripslashes($value);
                }
            }
        }
    }

    /**
     * Get this request's query string
     * @return string
     */
    public function getQueryString() {
        return http_build_query($this->get()->toArray(), '', '&');
    }

    /**
     * Loads this request with the current actual PHP request
     * @param RaiseApplication $application The application to load the request for
     * @return RaiseHttpRequest
     */
    public static function loadCurrent($application = null) {
        if(self::$current === false){
            self::$current = new RaiseHttpRequest();
            self::$current->application = $application;
            if($application instanceof RaiseApplication){
                self::$current->application->plugin()->callHook('raise.http.request.current.loading', self::$current);
            }
            self::$current->method($_SERVER['REQUEST_METHOD']);
            self::$current->uri($_SERVER['REQUEST_URI']);
            self::$current->version($_SERVER['SERVER_PROTOCOL']);
            // changed to stream to prevent Denial Of Service
            self::$current->body(new RaiseFileStream('php://input'));
            self::$current->time(RaiseDateTime::fromTimestamp($_SERVER['REQUEST_TIME']));
            if(array_key_exists('HTTP_HOST', $_SERVER)){
                self::$current->headers()->add('Host', $_SERVER['HTTP_HOST'], true);
            }
            if(array_key_exists('HTTP_REFERER', $_SERVER)){
                self::$current->headers()->add('Referer', $_SERVER['HTTP_REFERER'], true);
            }
            if(array_key_exists('HTTP_CONNECTION', $_SERVER)){
                self::$current->headers()->add('Connection', $_SERVER['HTTP_CONNECTION'], true);
            }
            if(array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)){
                self::$current->headers()->add('Accept-Language', $_SERVER['HTTP_ACCEPT_LANGUAGE'], true);
            }
            if(array_key_exists('HTTP_ACCEPT_ENCODING', $_SERVER)){
                self::$current->headers()->add('Accept-Encoding', $_SERVER['HTTP_ACCEPT_ENCODING'], true);
            }
            if(array_key_exists('HTTP_ACCEPT_CHARSET', $_SERVER)){
                self::$current->headers()->add('Accept-Charset', $_SERVER['HTTP_ACCEPT_CHARSET'], true);
            }
            if(array_key_exists('HTTP_ACCEPT', $_SERVER)){
                self::$current->headers()->add('Accept', $_SERVER['HTTP_ACCEPT'], true);
            }
            if(array_key_exists('HTTP_USER_AGENT', $_SERVER)){
                self::$current->headers()->add('User-Agent', $_SERVER['HTTP_USER_AGENT'], true);
            }

            foreach ($_COOKIE as $k => $v) {
                self::$current->cookies()->add($k, $v);
            }

            foreach ($_POST as $k => $v) {
                self::$current->post()->add($k, $v);
            }

            foreach ($_GET as $k => $v) {
                self::$current->get()->add($k, $v);
            }

            self::$current->https(RaiseServer::https());
            if($application instanceof RaiseApplication){
                self::$current->application->plugin()->callHook('raise.http.request.current.loaded', self::$current);
            }
        }
        return self::$current;
    }

    /**
     * Returns the full URL of this request
     * @return Url
     */
    public function getUrl(){
        $u = new Url();
        $u->host($this->headers->get('Host'));
        $u->scheme('http'.($this->https() ? 's' : ''));
        $markpos = strpos($this->uri(), '?');
        if($markpos !== false){
            $u->path(substr($this->uri(), 0, $markpos));
        }else{
            $u->path($this->uri());
        }
        $u->params($this->get()->toArray());
        return $u;
    }

}
